package org.io4j.filesystem;

import java.io.File;

import org.io4j.util.Argument;

public abstract class FileSystemObject {

	private final Path path;

	FileSystemObject(Path path) {
		Argument.isNotNull(path, "Path is null");
		Argument.isNot(path.isEmpty(), "Path is empty");
		this.path = path;
	}

	/**
	 * Tests whether this directory or file exists.
	 * <p>
	 * {@code Directory} or {@code File} is required to exist in the file system
	 * when constructed. It can however be deleted either by calling
	 * {@link #delete()} or by some other process.
	 * 
	 * @return {@code true} if the directory or file exists; {@code false}
	 *         otherwise
	 */
	public boolean exists() {
		return path.exists();
	}

	FileSystemObject rename(String name) {
		if (name.equals(path.getName())) {
			return this;
		}
		return move(path.rename(name));
	}

	public abstract FileSystemObject move(Path destinationPath);

	void doMove(Path destinationPath) {
		if (!path.toFile().renameTo(destinationPath.toFile())) {
			throw new FileSystemException(toString() + " could not be moved to " + destinationPath);
		}
	}

	FileSystemObject move(Directory destinationDirectory) {
		if (destinationDirectory.contains(this)) {
			return this;
		}
		return move(destinationDirectory.getPath().createChild(getName()));
	}

	boolean destinationPathEqualsCurrent(Path destinationPath) {
		return getPath().equalsResolved(destinationPath);
	}

	public abstract FileSystemObject copy(Path destinationPath);

	public abstract FileSystemObject copy(Directory destinationDirectory);

	public abstract boolean delete();

	static void ensureDirectoryExists(Path directoryPath) {
		if (!directoryPath.exists() && directoryPath.hasParent()) {
			ensureDirectoryExists(directoryPath.getParent());
			if (!directoryPath.toFile().mkdir()) {
				throw new FileSystemException("Directory " + directoryPath + " could not be created.");
			}
		}
	}

	/**
	 * Returns {@code Path} location of this file system object.
	 */
	public Path getPath() {
		return path;
	}

	/**
	 * Returns the name of this file system object (last element of its location
	 * path).
	 */
	public String getName() {
		return getPath().getName();
	}

	/**
	 * Returns the size of this file system object.
	 * 
	 * @return size in bytes
	 */
	public long getSize() {
		return path.toFile().length();
	}

	/**
	 * Returns modification timestamp for this file system object.
	 * 
	 * @return Number of milliseconds since the epoch (00:00:00 GMT, January 1,
	 *         1970) or 0 if file doesn't exist or other problem occurred while
	 *         trying to obtain timestamp information.
	 * @see java.io.File#lastModified()
	 */
	public long getModificationTimestamp() {
		return path.toFile().lastModified();
	}

	/**
	 * Sets modification timestamp on this file system object.
	 * 
	 * @param timestamp
	 *           Number of milliseconds since the epoch (00:00:00 GMT, January 1,
	 *           1970)
	 * @throws IllegalArgumentException
	 *            if timestamp is negative
	 * @throws FileSystemException
	 *            if setting modification timestamp failed
	 * @see java.io.File#setLastModified(long)
	 */
	public void setModificationTimestamp(long timestamp) {
		Argument.is(timestamp >= 0, "Timestamp has to be greater than zero (is " + timestamp + ")");
		if (!path.toFile().setLastModified(timestamp)) {
			throw new FileSystemException("Could not set modification timestamp for " + path);
		}
	}

	/**
	 * Updates modification timestamp of this file system object with current
	 * system time as number of milliseconds since the epoch (00:00:00 GMT,
	 * January 1, 1970).
	 */
	public void updateModificationTimestamp() {
		setModificationTimestamp(System.currentTimeMillis());
	}

	/**
	 * Tests whether this file system object is hidden.
	 * 
	 * @return {@code true} if this file system object is hidden, {@code false}
	 *         otherwise
	 */
	public boolean isHidden() {
		return path.toFile().isHidden();
	}

	/**
	 * Tests whether this file system object is readable.
	 * 
	 * @return {@code true} if this file system object is readable, {@code false}
	 *         otherwise
	 */
	public boolean isReadable() {
		return path.toFile().canRead();
	}

	/**
	 * Tests whether this file system object is writable.
	 * 
	 * @return {@code true} if this file system object is writable, {@code false}
	 *         otherwise
	 */
	public boolean isWritable() {
		return path.toFile().canWrite();
	}

	/**
	 * Tests whether this file system object is executable.
	 * 
	 * @return {@code true} if this file system object is executable,
	 *         {@code false} otherwise
	 */
	public boolean isExecutable() {
		return path.toFile().canExecute();
	}

	/**
	 * Returns {@code java.io.File} representation of this file or directory.
	 * Note that {@code java.io.File} does not differentiate between directory
	 * and file objects in the file system
	 * 
	 * @return {@code java.io.File} representing this directory and file
	 */
	public File toFile() {
		return path.toFile();
	}

	/**
	 * Returns {@code String} representation of this directory or file.
	 * 
	 * @return {@code String} representing this directory or file based on
	 *         {@code String} form of its path
	 */
	@Override
	public String toString() {
		return path.toString();
	}

	@Override
	public final int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((path == null) ? 0 : path.hashCode());
		return result;
	}

	@Override
	public final boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof FileSystemObject)) {
			return false;
		}
		FileSystemObject other = (FileSystemObject) obj;
		if (path == null) {
			if (other.getPath() != null) {
				return false;
			}
		} else if (!path.equals(other.getPath())) {
			return false;
		}
		return true;
	}

	int compareTo(FileSystemObject other) {
		return path.compareTo(other.getPath());
	}
}
